package com.meego.common.config;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.SortedMap;
import java.util.TreeMap;

import ops.ga.crypto.Crypto3DES;
import ops.ga.crypto.CryptoRSA;
import ops.ga.log4j.GA;
import ops.ga.util.ConvertData;

@SuppressWarnings({ "unchecked", "rawtypes" })
public class Config {
	private static Properties props;
	private static Config sConfig;
	private static String configFile;
	private static SortedMap mConfigMap;
	private static HashSet<ConfigReloadListener> mConfigReloadListeners;

	/**
	 * 
	 * @return Return a Config object. This object do not store any
	 *         configuration until method <b>loadConfig</b> is called
	 */
	public static Config getInstance() {
		if (sConfig == null)
			createInstance();
		return sConfig;
	}

	private static synchronized void createInstance() {
		if (sConfig == null) {
			sConfig = new Config();
		}
	}

	/**
	 * Load system configuration to memory.
	 * 
	 * @param filePath
	 *            The path to file which store system configuration.
	 */
	public void loadConfig(String filePath) throws Exception {
		configFile = filePath;
		Properties newConfig = new Properties();
		try {
			loadPropertiesFile(newConfig, configFile);
			synchronized (props) {
				setProps(newConfig);
				getProps().put("sys.cpus", String.valueOf(Runtime.getRuntime().availableProcessors()));
				getProps().put("java.version", System.getProperty("java.version"));
			}
			synchronized (mConfigMap) {
				mConfigMap = new TreeMap(newConfig);
			}
			notifyConfigReload();
		} catch (Exception e) {
			e.printStackTrace();
			System.out.println("Can not load config file");
			e.printStackTrace();
			newConfig = null;
			throw e;
		}

		System.out.println("LOADED CONFIGURATION FILE: " + filePath + " SUCCESSFULLY");
	}

	/**
	 * Reload system configuration
	 */
	public void reloadConfig() {
		Properties newConfig = new Properties();
		try {
			loadPropertiesFile(newConfig, configFile);
		} catch (Exception e) {
			e.printStackTrace();
			System.out.println("Can not load config file");
			e.printStackTrace();
			newConfig = null;
		}
		if (newConfig == null) {
			GA.config.error("Has error on load new config, system will continous use old config");
		} else {
			synchronized (props) {
				setProps(newConfig);
				getProps().put("sys.cpus", String.valueOf(Runtime.getRuntime().availableProcessors()));
				getProps().put("java.version", System.getProperty("java.version"));
			}
			synchronized (mConfigMap) {
				mConfigMap = new TreeMap(newConfig);
			}
			notifyConfigReload();
		}
	}

	private void loadPropertiesFile(Properties prop, String filePath) throws Exception {
		if (prop == null)
			prop = new Properties();
		try {
			BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8"));
			String text = null;
			// repeat until all lines is read
			while ((text = reader.readLine()) != null) {
				if (text.startsWith("!include")) {
					text = text.substring(8).trim();
					loadPropertiesFile(prop, text);
				} else
//					prop.load(new ByteArrayInputStream(text.getBytes("UTF-8")));
					prop.load(reader);
			}

		} catch (FileNotFoundException e) {
			System.out.println("[ConfigData] CAN NOT READ CONFIG FILE - FILE NOT FOUND: " + filePath);
			e.printStackTrace();
			throw e;
		} catch (IOException e) {
			System.out.println("[ConfigData] CAN NOT READ CONFIG FILE - IOException");
			e.printStackTrace();
			throw e;
		}
	}

	protected Config() {
		setProps(new Properties());
		mConfigMap = new TreeMap();
		mConfigReloadListeners = new HashSet<ConfigReloadListener>();
	}

	/**
	 * Searches for the property with the specified key which stored in
	 * configuration file.
	 * 
	 * @param key
	 *            the specified key used for search in configuration.
	 * @return The String for the resulting configuration value.
	 * @throws ConfigKeyNotFoundException
	 *             if the key is not found.
	 */
	public static String getStrConfig(String key) throws ConfigKeyNotFoundException {
		String result = getNullConfig(key);

		if (result == null) {
			if (GA.config != null)
				GA.config.debug("Config key " + key + " not found");
			throw new ConfigKeyNotFoundException(key);
		}
		return result;
	}

	/**
	 * Searches for the property with the specified key which stored in
	 * configuration file.
	 * 
	 * @param key
	 *            the specified key used for search in configuration.
	 * @param def
	 *            Default value if this key is not found.
	 * @return The String for the resulting configuration value. <b>def</b> if
	 *         the key is not found.
	 */
	public static String getStrConfig(String key, String def) {
		if (key == null)
			return def;
		String result = getNullConfig(key);
		if (result == null) {
			if (GA.config != null)
				GA.config.debug("Config key " + key + " not found");
			return def;
		}
		return result;
	}

	/**
	 * Searches for the property with the specified key which stored in
	 * configuration file.
	 * 
	 * @param key
	 *            the specified key used for search in configuration.
	 * @return The list of strings for the resulting configuration value
	 */
	public static List<String> getListConfig(String key) {
		return getListConfig(key, null, ",");
	}

	/**
	 * Searches for the property with the specified key which stored in
	 * configuration file.
	 * 
	 * @param key
	 *            the specified key used for search in configuration.
	 * @param def
	 *            Default string, used to convert to list when the key is not
	 *            found
	 * @return The list of strings for the resulting configuration value
	 */
	public static List<String> getListConfig(String key, String def) {
		return ConvertData.getInstance().toList(getStrConfig(key, def));
	}

	/**
	 * Searches for the property with the specified key which stored in
	 * configuration file.
	 * 
	 * @param key
	 *            the specified key used for search in configuration.
	 * @param def
	 *            Default string, used to convert to list when the key is not
	 *            found
	 * @param regex
	 *            : The regular expression is used to split the string
	 * @return The list of strings for the resulting configuration value
	 */
	public static List<String> getListConfig(String key, String def, String regex) {
		return ConvertData.getInstance().toList(getStrConfig(key, def), regex);
	}

	/**
	 * Searches for the property with the specified key which stored in
	 * configuration file.
	 * 
	 * @param key
	 *            the specified key used for search in configuration.
	 * @return <b>true</b>(or <b>false</b>) if the <b>value</b> matches of the
	 *         given regular expression.
	 * @throws ConfigKeyNotFoundException
	 *             - if the key is not found.
	 * @throws ConfigInvalidValueException
	 *             - if the value does not match of the given regular
	 *             expression.
	 */
	public static boolean getBoolConfig(String key) throws ConfigKeyNotFoundException, ConfigInvalidValueException {
		String strResult = getStrConfig(key);
		if (("1".equals(strResult)) || ("true".equalsIgnoreCase(strResult)))
			return true;
		if (("0".equals(strResult)) || ("false".equalsIgnoreCase(strResult)))
			return false;
		throw new ConfigInvalidValueException(key, strResult);
	}

	/**
	 * Searches for the property with the specified key which stored in
	 * configuration file.
	 * 
	 * @param key
	 *            the specified key used for search in configuration.
	 * @param def
	 *            Default value if this key is not found or the value does not
	 *            match of the given regular expression.
	 * @return <b>true</b>(or <b>false</b>) if the value matches of the given
	 *         regular expression.<br/> <b>def</b>: if <b>key</b> is not found
	 *         or <b>value</b> is invalid
	 */
	public static boolean getBoolConfig(String key, boolean def) {
		String strResult = getNullConfig(key);
		if (strResult == null)
			return def;
		if (("1".equals(strResult)) || ("true".equalsIgnoreCase(strResult)))
			return true;
		if (("0".equals(strResult)) || ("false".equalsIgnoreCase(strResult)))
			return false;
		return def;
	}

	/**
	 * Searches for the property with the specified key which stored in
	 * configuration file.
	 * 
	 * @param key
	 *            the specified key used for search in configuration.
	 * @return The <b>int</b> value for the resulting configuration value
	 * @throws ConfigKeyNotFoundException
	 *             - if the key is not found.
	 * @throws ConfigInvalidValueException
	 *             - if the value does not match of the given regular
	 *             expression.
	 */
	public static int getIntConfig(String key) throws ConfigKeyNotFoundException, ConfigInvalidValueException {
		String strResult = getStrConfig(key);
		try {
			return Integer.parseInt(strResult.trim());
		} catch (NumberFormatException ex) {
		}
		throw new ConfigInvalidValueException(key, strResult);
	}

	/**
	 * Searches for the property with the specified key which stored in
	 * configuration file.
	 * 
	 * @param key
	 *            the specified key used for search in configuration.
	 * @param def
	 *            Default value if this key is not found or the value does not
	 *            match of the given regular expression.
	 * @return <b>int</b> value if the value matches of the given regular
	 *         expression.<br/> <b>def</b>: if <b>key</b> is not found or
	 *         <b>value</b> is invalid
	 */
	public static int getIntConfig(String key, int def) {
		if (key == null)
			return def;
		String strResult = getNullConfig(key);
		if (strResult == null)
			return def;
		try {
			return Integer.parseInt(strResult.trim());
		} catch (NumberFormatException ex) {
		}
		return def;
	}

	public static String getNullConfig(String aKey) {
		if (props == null) {
			GA.config.error("Config not yet loaded");
			return null;
		}
		String result = (String) props.get(aKey);
		String encType = (String) props.get(aKey + ".enctype");
		if ("3des".equals(encType)) {
			GA.config.debug("Encrypted 3DES Config Item found");
			try {
				result = Crypto3DES.getInstance().base64Decrypt(result);
			} catch (Exception e) {
				GA.config.error("Decrypt config fail", e);
			}
		}
		if ("rsa".equals(encType)) {
			GA.config.debug("Encrypted RSA Config Item found");
			try {
				result = CryptoRSA.getInstance().base64Decrypt(result);
			} catch (Exception e) {
				GA.config.error("Decrypt config fail", e);
			}
		}

		if (result != null) {
			if (GA.config != null)
				GA.config.debug("Config key " + aKey + ":" + result);
		} else {
			if (GA.config != null)
				GA.config.debug("Config key " + aKey + ":<null>");
		}
		return result;
	}

	/**
	 * When create a Class that use the configuration, this class must implemnet
	 * ConfigReloadListener and add to ConfigReloadListener
	 * 
	 * @param aListener
	 */
	public static void addConfigReloadListener(ConfigReloadListener aListener) {
		synchronized (mConfigReloadListeners) {
			mConfigReloadListeners.add(aListener);
		}
	}

	public void removeConfigReloadListener(ConfigReloadListener aListener) {
		synchronized (mConfigReloadListeners) {
			mConfigReloadListeners.remove(aListener);
		}
	}

	/**
	 * use to reload all configuration in system
	 */
	public static void notifyConfigReload() {

		Iterator<ConfigReloadListener> i;
		synchronized (mConfigReloadListeners) {
			i = mConfigReloadListeners.iterator();
		}

		while (i.hasNext())
			((ConfigReloadListener) i.next()).configReloaded();
	}

	static {
		createInstance();
	}

	public static Map getMapConfig(String prefix) {
		if (mConfigMap == null) {
			GA.config.error("Config not yet loaded when fetching map " + prefix);
			return null;
		}

		Map result = new HashMap();
		Iterator scan = mConfigMap.subMap(prefix + ".", prefix + "/").entrySet().iterator();
		prefix = prefix + ".";
		while (scan.hasNext()) {
			Map.Entry e = (Map.Entry) scan.next();
			String key = (String) e.getKey();
			if (key.startsWith(prefix))
				result.put(key.substring(prefix.length()), e.getValue());
		}
		GA.config.debug("Config map " + prefix + ":" + result);
		return result;
	}

	public void setProps(Properties props) {
		Config.props = props;
	}

	public Properties getProps() {
		return props;
	}
}
